iT邦幫忙

2026 iThome 鐵人賽

DAY 10
1
Claude AI

AI 負責答,我負責讓答案可信——公部門工程師的 30 天 Claude 協作紀律系列 第 10 篇

Day 10:一個 build 設定,最後搞出 46 萬個檔案

  • 分享至 

  • xImage
  •  

Day 10:一個 build 設定,最後搞出 46 萬個檔案

今天這個事故有兩個層次。第一層是「物」——一個設定本身沒錯、卻在重複執行下累積成災的坑;第二層是「人」——清理這個爛攤子的過程,是我跟 AI 協作至今,human-in-the-loop 展現得最徹底的一次。我把它們合在一天講,因為它們其實是同一件事的一體兩面:坑怎麼來的,決定了為什麼 AI 清不動它。

現象:資料夾長到刪不完

事情發生在把一個舊專案改用 Vite(一套前端建置工具)的時候。某次建置後,我發現輸出目錄不對勁——它沒有正常產出該有的檔案,而是變成一層一層自我巢狀的目錄:

public/js/dist/js/dist/js/dist/js/dist/……(一路巢狀下去)

我開始刪,但刪到一半發現深度好像沒在減少。一度以為只是「刪除很慢」,後來才發現,背景還有一個沒關掉的建置程序在跑,一邊往同一個目錄寫、一邊跟我的刪除動作互相拉扯。 巢狀深度先測到 2263 層,這已經比誰的直覺都誇張——但這還只是「深度」,不是「規模」。等真正清完之後才知道,這棵樹底下實際埋了多少東西:77,500 個目錄、46 萬 4 千多個檔案,總共 11.8 GB。 深度只是表象,真正的災情是躺在裡面的檔案總數,那是要等清理工具跑完整整 2.5 小時之後才現形的數字。

根因:outDir 落在 publicDir 裡面(而且這不是 AI 的錯)

追出來的原因,是一個很經典的 Vite 設定錯誤。Vite 每次 build 會做一件事:把靜態資源目錄(publicDir,預設 public/)的內容整份複製到輸出目錄(outDir)。 而我把 outDir 設成了 public/js/dist——它落在 public/ 的裡面。於是每次 build 就變成一個自我累積的迴圈:複製 public/ 時,連同上一輪產生的 js/dist/ 一起複製進去,再疊上這次的新產物。每 build 一次,就多巢狀一層。

真正讓它爆炸的,是那個沒關掉的背景 watch 程序——它偵測到檔案變動就自動重建,而它自己製造的巢狀目錄又被算成「檔案變動」,於是觸發下一次重建……一個自我餵養的迴圈就此展開,短時間內疊出天文數字的層數。

這裡要先講一件公道話:這個坑,嚴格說不是 AI 犯的錯,也不是「AI 協作」特有的問題。 它是 Vite 設定的經典陷阱,任何一個不熟這個設定的人,自己動手也會踩到同樣的坑。我把它放進這個系列,不是為了說「你看 AI 又錯了」,而是因為它帶出一個更值得記住的模式,以及——它製造出的那個爛攤子,考驗的正是人機協作。

那個模式是:「累積型副作用」是最難防的一類 bug。 一般的 bug 你執行一次就會發現,但累積型副作用「單次執行」完全正常:build 一次,你看到多了一層,覺得沒什麼。它要重複很多次、或搭上一個自動迴圈,才會顯現破壞力。等你發現時,它已經滾成難以收拾的規模。這類坑之所以難防,是因為審視程式碼時,我們(還有 AI)問的通常是「這樣寫對不對」——而它單次執行是對的。真正該問的是另一個問題:「這個動作重複執行一千次,會發生什麼?」 這也呼應了 Day 03 講 Promise.all 時的「先問規模」——只是那次的規模是資料量(200 筆),這次的規模是時間與次數(build 一千次)。

清理:這不是一鍵就能刪的爛攤子

你可能會想,深一點的資料夾,一個指令刪掉不就好了?

第一次嘗試就直接碰壁:用 Windows 內建的 rd /s /q 遞迴刪除,結果直接以 stack overflow(0xC00000FD)當場崩潰。原因是這個內建指令的遞迴刪除是用「呼叫堆疊」實作的,2500 多層的深度直接把堆疊炸掉。這也解釋了後來為什麼改用 robocopy——它是迭代式處理,不會有堆疊爆掉的問題,但也因為量體太大,得跑上好幾個小時。所以這不是「刪不刪得掉」的問題,是「用什麼工具、什麼方式,才不會自己先當機」的問題。而在漫長的清理過程裡,AI 給的每一個判斷,我都不能照單全收——它接連幾次都需要被現場事實修正。

幾次失準,幾次校正

第一次:估錯規模的量級。 我問 AI 大概還剩幾層,它估「三十幾層」。但實際深掃之後是 2263 層,直接多了兩個數量級。它是憑「一般情況」在猜,不是根據現場。

第二次:連自己的量測方法都靠不住。 清理跑到一半,深度數字不減反增:2263 → 2562 → 2672。乍看之下很嚇人,好像巢狀還在長。但這次 AI 沒有照單全收自己量到的數字,反而先質疑了自己:這個階段只有刪除動作在跑,沒有任何東西會再生成新的巢狀層,深度不可能真的變深。 真正的原因是,它拿「即時 stat 目錄」的方式去量測,卻正好跟 robocopy 用 32 個執行緒同時在同一棵樹上動刀互相打架,兩邊搶著讀寫,量到的只是不穩定的競態快照。它自己說了一句很誠實的話:「這個我沒辦法採信。」——連自己的診斷工具在什麼情境下會失真,都要保持懷疑。

第三次:監控反而製造新問題。 它建議開一個 log 來追蹤刪除進度,這聽起來很合理。但那份 log 在漫長的執行過程裡不斷寫入,最後膨脹到 867MB——監控這個動作本身,變成了另一個要清理的東西。

第四次:加速方案被一句話戳破。 它想用多執行緒參數(/MT:32)加速清理。我看了一眼實際狀況,回它一句:「我看裡面都是 js、dist、js、dist 這樣的空資料夾而已,這是正常的嗎?」它查證後才反應過來——這是正常現象,真正佔容量的檔案內容早就被刪光了,現在剩下的只是又窄又深、每一層都是空的巢狀鏈。32 個執行緒對這種形狀的樹沒有東西可以平行處理,反而是彼此互搶同一條路徑,增加額外開銷。它的加速方案,對這個特定形狀完全不適用,而它一開始沒看出來,因為它不知道「裡面已經是空的」。

第五次:講出危險操作,又自己收回。 過程中它一度提出「直接砍掉整個磁碟區塊層級」的做法。我不懂這是什麼,追問了一句。它下一輪就誠實澄清:這個說法不夠精確,檔案系統(NTFS)根本沒有「只清空某個資料夾底層區塊」這種操作,這其實不是一個能安全用在這裡的做法,請當它沒說過。——如果我當時沒追問、直接照做,後果不堪設想。「多問一句這是什麼意思」,是這裡最便宜也最值錢的一步。

最後:真正有效的清理,是把找到的殘留背景行程徹底砍掉、讓 robocopy 用獨立、不受這個工具背景機制干擾的方式跑到底。 兩個半小時後,它交出了前面那份完整的戰報——77,500 個目錄、46 萬 4 千多個檔案、11.8 GB。這時才真正確認:這場災難的規模,從頭到尾沒有人事先估對過,包括 AI 自己。

誰在真正解決問題?

把這五次擺在一起看,有一件事很清楚:真正推動問題解決的每一個關鍵事實,都是我提供的。

是我說「都是空資料夾」,才擋掉無效的多執行緒方案;是我追問那個危險操作,才避免一次災難。AI 在這整件事裡,負責的是「執行清理指令」「解釋為什麼會這樣」,而且它也展現了很重要的一面——連自己量到的數字都保持懷疑,發現深度讀數不合理時,沒有照單全收,而是先質疑量測方法本身有沒有問題。但「現在的真實狀況到底是什麼」,它終究看不到,只能靠我餵;真正的規模,也是靠工具老老實實跑完兩個半小時才現形,沒有任何一次事前的猜測是準的。

這就是 human-in-the-loop 最真實的樣子。它不浪漫,甚至有點狼狽——不是 AI 優雅地幫你解決問題,而是你得在旁邊,一次次檢查它的判斷、用眼前的事實把它拉回正軌。方向盤從頭到尾在人手上,AI 是那個很有力氣、但看不到路況的引擎。

這一天的分工:AI 看不到「現在」

Day 07 我說過,human-in-the-loop 是分工設計,也需要接口對齊。今天這個案例,把 AI 那個先天盲點講得更具體了:它看不到「現在的真實狀況」。

它不知道那些資料夾是空的、不知道背景程序還在跑、不知道 log 爆了、不知道那個「磁碟區塊」操作會傷到別的專案。這些都是當下的、一手的現場資訊,只存在於盯著螢幕的那個人眼裡。AI 只能根據你給它的資訊推理——你給得準,它就準;你漏講了關鍵的一項,它就會很有自信地把你帶往錯的方向。

所以人在這裡貢獻的那個「大於」,是當下的觀察力:看得出「這裡不對」、報得出「現在是這個狀況」、忍得住「先追問再動手」。這三件事,AI 都替代不了。它負責力氣,你負責眼睛和剎車——少了任何一個,這個爛攤子要嘛清不掉,要嘛清出更大的災難。


明天離開這組 Vite 事故,進入資料庫的世界:一個空字串,怎麼讓整張資料表被鎖死、拖垮所有相關作業,以及 AI 怎麼不靠復現、光憑證據的蛛絲馬跡就把範圍一步步縮小。


上一篇
Day 9:AI 有些東西就是產不準——從畫不出台灣,到判斷不了浮水印
下一篇
Day 11:一個空字串,怎麼讓資料庫全表鎖死
系列文
AI 負責答,我負責讓答案可信——公部門工程師的 30 天 Claude 協作紀律 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
helenanova
iT邦新手 5 級 ‧ 2026-09-22 10:11:52

「這個動作重複執行一千次,會發生什麼?」這個問法很值錢。這類坑我會多加一道兩行的防線:build 開始前直接檢查 outDir 不在 publicDir 裡面(廣義說:輸出路徑不能落在任何輸入路徑之下),設定錯誤在第一次 build 前就爆炸,而不是第兩千層才被發現。watch 那邊同理,把 outDir 加進 ignore,自我餵養的迴圈就斷了——你這個案例等於同時踩了「累積型副作用」和「自體觸發」兩個半邊。

感謝指教,我把「輸出路徑不能落在任何輸入路徑之下」這段話加在 claude.md 的 vite.config.js 的規則中了

加進 claude.md 是好的第一步。不過給 agent 看的規則本質是建議,大多時候會遵守,但沒有強制力。真正會擋下來的還是 build 前的檢查,例如在 vite.config.js 頂層放:

const rel = path.relative(path.resolve(publicDir), path.resolve(outDir));
if (rel === '' || !rel.startsWith('..')) throw new Error('outDir must not be inside publicDir');

兩層都放,agent 不太會寫錯設定;萬一寫錯,第一次 build 前就爆炸。

了解,感謝說明

我要留言

立即登入留言